package org.ands.xml;

import java.io.File;
import java.io.InputStream;
import java.io.PrintStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Map.Entry;
import javax.xml.namespace.NamespaceContext;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.ands.common.Utilities;
import org.ands.input.IInputHelper;
import org.jsoup.Jsoup;
import org.w3c.dom.DOMException;
import org.w3c.dom.DOMImplementation;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import org.jsoup.select.Elements;


// TODO: Auto-generated Javadoc
/**
 * Does useful operations on a org.w3c.dom.
 *  
 * @author      Melanie Adamson
 * @version     1.0                   
 * @since       2012-07-20
 */
public class DOMHelper extends IInputHelper {
	
	public DOMHelper() {
	}
	
	public Element docElement(){
		assert(_document != null);
		assert(_document.getDocumentElement() != null);
		return _document.getDocumentElement();
	}
	
	
//	public final Element root(){
//		assert(_document != null);
//		assert(_document.getDocumentElement() != null);
//		assert(_nsContext != null);
//		return _document.getDocumentElement();
//	}
//	
	public void appendToRoot(final Node node){
		
		assert(_document != null);
		assert(_document.getDocumentElement() != null);
		
		Node adoptedNode = _document.adoptNode(node);
		_document.getDocumentElement().appendChild(adoptedNode);
	}
	
	public String namespaceUri(){
		assert(_document != null);
		assert(_document.getDocumentElement() != null);
		String namespaceUri = _document.getDocumentElement().getNamespaceURI();
		assert(namespaceUri != null);
		assert(!namespaceUri.isEmpty());
		return namespaceUri;
	}
	
	public final NamespaceContext namespaceContext(){
		assert(_nsContext != null);
		return _nsContext;
	}
	
	Document _document;
	NamespaceContext _nsContext;
	
	/**
	 * Constructs a DOM DocumentBuilder in the way that we need them,
	 * consistently.
	 * 
	 * @return DocumentBuilder New DOM DocumentBuilder
	 * @throws Exception
	 *             the exception
	 * @since 1.0
	 */
	protected static DocumentBuilder documentBuilderInstance() throws Exception
	{
		DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
		if(dbf == null){
			throw new Exception("Failed to construct DocumentBuilderFactory");
		}
		
		dbf.setNamespaceAware(true);
		dbf.setCoalescing(true);
		dbf.setIgnoringElementContentWhitespace(true);
		dbf.setIgnoringComments(true);
		DocumentBuilder docBuilder = dbf.newDocumentBuilder();
		if(docBuilder == null){
			throw new Exception("Failed to construct document builder");
		}
		
		return docBuilder;
	}
	
	/**
	 * Constructs a DOM Document in the way that we need them, consistently.
	 * 
	 * @param  namespaceURI - ok if null
	 * @return document New DOM Document
	 * @throws Exception
	 *             the exception
	 * @since 1.0
	 */
	protected Document newDocument(
			final String namespaceURI, 
			final String qualifiedName,
			final Map<String, String> docRootAttributeMap) throws Exception{
		
		System.out.println("Request to construct document with namespaceURI "+namespaceURI+" and qualified name "+qualifiedName);
		DocumentBuilder builder = DOMHelper.documentBuilderInstance();
		DOMImplementation domImplementation = builder.getDOMImplementation();
		
		Document doc = domImplementation.createDocument(namespaceURI, qualifiedName, null);
		Element root = doc.getDocumentElement();
		
		
		assert(root.getNamespaceURI() != null);
		assert(!root.getNamespaceURI().isEmpty());
		assert(root.getNamespaceURI().equals(namespaceURI));
		
		Node docRoot = doc.getFirstChild();
		assert(docRoot.getNodeName() == qualifiedName);
		
		NamedNodeMap attributes = docRoot.getAttributes();
		assert(attributes.getLength() <= 0); 
		root.setAttribute("xmlns", namespaceURI);
		
		Iterator<Entry<String, String>> it = docRootAttributeMap.entrySet().iterator();
	    while (it.hasNext()) {
	        Map.Entry<String, String> pairs = (Map.Entry<String, String>)it.next();
	        System.out.println(pairs.getKey() + " = " + pairs.getValue());
	        String name = pairs.getKey();
	        assert(name != null);
	        assert(!name.isEmpty());
	        
	        String value = pairs.getValue();
	        assert(value != null);
	        assert(!value.isEmpty());
	        
	        System.out.println("Setting attribute on doc element, name: "+name+", value: "+value);
	        
	        ((Element)doc.getFirstChild()).setAttribute(name, value);
	    }
		
		return doc;
		
	}
	
	public void writeToFile(final String xmlOutFileName) throws Exception{
		assert(_document != null);
		
		if(!_document.hasChildNodes()){
			throw new Exception("Document has no children");
		}
		
		File xmlOutFile = new File(xmlOutFileName);
		if(!xmlOutFile.exists()){
			assert(xmlOutFile != null);
			xmlOutFile.createNewFile();
		}
		
		PrintStream printStream = new PrintStream(xmlOutFile);
		XmlWriter writer = new XmlWriter();
		writer.writeToStream(_document, printStream);
		
		String namespaceUri = _document.getDocumentElement().getNamespaceURI();
		assert(namespaceUri != null);
		assert(!namespaceUri.isEmpty());
		System.out.println("XML for namespace "+namespaceUri+" has been written to ["+xmlOutFileName+"].");
	}
	
		
	
	
	/**
	 * Throws if xpath contains illegal characters. Those acceptable are :
	 * alphanumaric, '_', '[', ']', '/', '.' , '@', ':'
	 * 
	 * @param xpath
	 *            the xpath
	 * @throws Exception
	 *             the exception
	 * @since 1.0
	 */
	public static void validateXpath(
			final String xpath) 
					throws Exception {
		
		if((xpath == null) || (xpath.length() <= 0)){
			throw new Exception("Requires valid xpath");
		}
		
		if(!xpath.matches("^[\\w_@:.*\\]\\[/]+$")){
			throw new Exception("xpath ["+xpath+"] contains illegal characters");
		}
		
	}
	
	/**
	 * Throws if tag name contains illegal characters. Those acceptable are :
	 * alphanumaric, '@' and ':' only
	 * 
	 * @param tagName
	 *            the tag name
	 * @param pattern
	 *            the pattern
	 * @throws Exception
	 *             the exception
	 * @since 1.0
	 */
	public static void validateTagName(
			final String tagName, 
			final String pattern) 
					throws Exception {
		
		if((tagName == null) || (tagName.length() <= 0)){
			throw new Exception("Requires valid tagName");
		}
		
		if(!tagName.matches(pattern)){
			throw new Exception("tagName ["+tagName+"] contains illegal characters - expecting pattern ["+pattern+"].");
		}
	}
	
	/**
	 * Create a node/attribute of name deduced from xPathTag if it doesn't
	 * exist; Only set its value if it is the last element in the xpath
	 * hierarchy. If it is the last element in the xpath hierarchy and it does
	 * not exist, set the node's value to value if it isn't all ready set.
	 * Append resultant node to parentNode provided. Only append to nodes of
	 * type ELEMENT_NODE or DOCUMENT_NODE
	 * 
	 * @param doc
	 *            the doc
	 * @param parentNode
	 *            the parent node
	 * @param xpathTag
	 *            the xpath tag
	 * @param value
	 *            the value
	 * @throws Exception
	 *             the exception
	 * @since 1.0
	 */
	private static void addXmlFinalNode(
			final String namespaceURI,
			Document doc,
			Node parentNode,  
			final String xpathTag, 
			final String value) 
					throws Exception{
		
		if(doc == null){
			throw new Exception("Invalid doc.");
		}
		
		if(parentNode == null){
			throw new Exception("Invalid parentNode.");
		}
		
		if(xpathTag == null){
			throw new Exception("Invalid xpathTag.");
		}
		
		if(value == null){
			throw new Exception("Invalid value.");
		}
		
		if(	(parentNode.getNodeType() != Node.ELEMENT_NODE) && 
			(parentNode.getNodeType() != Node.DOCUMENT_NODE) ){
			throw new Exception("Unable to append to parent ["+ parentNode.getNodeName()+"] of type ["+parentNode.getNodeType()+"].");
		}
		
		int index = getLastIndex(xpathTag);
		String name = getTag(xpathTag);
		assert(name != null);
		assert(name.length() > 0);
		
		if(name.substring(0, 1).equals("@")){	// Attribute 
			String attrName = name.substring(1);
			Element element = (Element)parentNode;
			String attrVal = element.getAttribute(attrName);
			if((attrVal == null) || (attrVal.length() <= 0) || (!attrVal.equals(value))){
				System.out.println("Setting attribute ["+attrName+"] to value ["+value+"] on element ["+element.getNodeName()+"].");
				element.setAttribute(attrName, value);
			}
			 
		}
		else{
			Node node = getElementAtIndex(parentNode, name, index);
			if(node == null){
				node = createElementAtIndex(namespaceURI, doc, parentNode, name, index);
				if(node == null){
					assert(false);
					throw new Exception("Unable to construct node ["+name+"] on parent ["+parentNode.getNodeName()+"].");
				}
			}
			
			assert(value != null);
			if(value.length() > 0){
				setElementText(doc, node, value);
			}
		}
		
	}
	
	/**
	 * Create a node/attribute of name deduced from xPathTag if it doesn't
	 * exist; Only append to nodes of type ELEMENT_NODE or DOCUMENT_NODE.
	 * 
	 * @param doc
	 *            the doc
	 * @param parentNode
	 *            the parent node
	 * @param xpathTag
	 *            the xpath tag
	 * @return the node
	 * @throws Exception
	 *             the exception
	 * @returns <code>Node</code> if Node successfully
	 *          found/constructed/amended; Throws <code>Exception</code>
	 *          otherwise.
	 * @since 1.0
	 */
	private static Node addXmlMiddleNode(
			final String namespaceURI,
			final Document doc,
			final Node parentNode,  
			final String xpathTag) 
					throws Exception{
		
		if(doc == null){
			throw new Exception("Invalid doc.");
		}
		
		if(parentNode == null){
			throw new Exception("Invalid parentNode.");
		}
		
		if(xpathTag == null){
			throw new Exception("Invalid xpathTag.");
		}
		
		if(	(parentNode.getNodeType() != Node.ELEMENT_NODE) && 
			(parentNode.getNodeType() != Node.DOCUMENT_NODE) ){
			throw new Exception("Unable to append to parent ["+ parentNode.getNodeName()+"] of type ["+parentNode.getNodeType()+"].");
		}
		
		int index = getLastIndex(xpathTag);
		String name = getTag(xpathTag);
		assert(name != null);
		assert(name.length() > 0);
		
		Node node = null;
		if(name.substring(0, 1).equals("@")){	// Attribute 
			throw new Exception("Unable to construct attribute ["+name+"] in the middle of a hierarchy.");
		}
		
		// Element, not attribute
		node = getElementAtIndex(parentNode, name, index);
		if(node == null){
			// Create a new element.  Provide the value if this is the last element in the xpath hierarchy.
			node = createElementAtIndex(namespaceURI, doc, parentNode, name, index);
			if(node == null){
				assert(false);
				throw new Exception("Unable to construct node ["+name+"] on parent ["+parentNode.getNodeName()+"].");
			}
		}
		
		return node; // Becomes parent for next level in hierarchy.
	}
	
	/**
	 * Creates <code>Element</code> with name provided, at index provided, with
	 * value provided. Appends to parent <code>Node</code> provided.
	 * 
	 * @param doc
	 *            the doc
	 * @param parentNode
	 *            the parent node
	 * @param name
	 *            the name
	 * @param index
	 *            the index
	 * @return the node
	 * @throws Exception
	 *             the exception
	 * @para)m name Name of <code>Node</code> to be created.
	 * @returns <code>Node</code> if Node found; <code>null</code> otherwise.
	 * @since 1.0
	 */
	private static Node createElementAtIndex(
			final String namespaceURI,
			Document doc, 
			Node parentNode, 
			final String name, 
			final int index) 
					throws Exception{
		
		assert(doc != null);
		assert(parentNode != null);
		assert(name != null);
		
		assert(getElementAtIndex(parentNode, name, index) == null);  // Confirm:  no element of this name at this index
		
		NodeList nodeList = parentNode.getChildNodes();
		int lastIndexMatching = 0; //
	    for (int i = 0; i < nodeList.getLength(); i++) {
	        Node currentNode = nodeList.item(i);
            if(currentNode.getNodeName().equals(name)){ 
            	// Found node of matching name
            	lastIndexMatching++;
            }
	    }
	    
	    assert(lastIndexMatching <= index);  // Confirm:  no element of this name at this index
	    Element lastElementAdded = null;
	    for(int i = 0; i < (index - lastIndexMatching); i++)
	    {
	    	lastElementAdded = doc.createElementNS(namespaceURI, name);
	    	//lastElementAdded = doc.createElement(name);
	    	//lastElementAdded.setPrefix(doc.getPrefix());
	    	//lastElementAdded = doc.createElement(name);
			assert(lastElementAdded != null);
			try{
				parentNode.appendChild(lastElementAdded);
				System.out.println("Appended element of ["+name+"] to parentNode ["+parentNode.getNodeName()+"]");
			}catch (DOMException e){
				throw new Exception("Failed to append element ["+name+"] to parentNode ["+parentNode.getNodeName()+"] - Exception ["+e.getMessage()+"].");
			}
	    }
		
	    assert(lastElementAdded != null); // Confirm:  element of this name at this index had to be created
		return lastElementAdded;
	}
	
	
	/**
	 * Seeks Node of name provided at index provided, on parentNode provided.
	 * 
	 * @param parentNode
	 *            the parent node
	 * @param name
	 *            the name
	 * @param index
	 *            the index
	 * @return the element at index
	 * @throws Exception
	 *             the exception
	 * @returns <code>Element</code> if Node found; <code>null</code> otherwise.
	 * @since 1.0
	 */
	private static Node getElementAtIndex(
			final Node parentNode, 
			final String name, 
			final int index) 
					throws Exception{
		
		assert(parentNode != null);
		assert(name != null);
		assert(name.length() > 0);
		
		NodeList nodeList = parentNode.getChildNodes();
		int currentIndexThisName = 0; // Element index begins at 1
	    for (int i = 0; i < nodeList.getLength(); i++) {
	        Node currentNode = nodeList.item(i);
	        assert(currentNode != null);
	        String nodeName = currentNode.getNodeName();
	        if(nodeName == null){
	        	System.out.println("Node name was not set for node "+nodeName);
	        	assert(false);
	        }
	          
	        if(nodeName.equals(name)){
            	currentIndexThisName++;
            	// Found node of matching name
            	if(currentIndexThisName == index){
            		// Found node of matching index
            		System.out.println("Found name "+nodeName);
            		return currentNode;
            	}
	        }
	        else{
	        	//System.out.println(nodeName+" does not equal "+name);	
	        }
	    }
	    
	    return null;
	}
	
	/**
	 * xpath may be of form:
	 * /registryObjects/registryObject[1]/@group,"Atlas of Living Australia".
	 * 
	 * @param tag
	 *            the tag
	 * @return index Index of this tag, for example: 1 if tag is
	 *         "registryObject[1]"; or 0 if tag is "registryObjects".
	 * @throws Exception
	 *             the exception
	 * @since 1.0
	 */
	public static int getLastIndex(
			final String tag) 
					throws Exception{
		
		// Determine index if there are square brackets
		if(tag.contains("[")){
			if(!tag.contains("]")){
				throw new Exception("tag ["+tag+"] contains \'[\' but no \']\'.");
			}
			
			int indexOfFirstBracket = tag.lastIndexOf("[");
			int indexOfSecondBracket = tag.lastIndexOf("]");
			assert(indexOfSecondBracket > indexOfFirstBracket);
			String indexText = tag.substring(indexOfFirstBracket+1, indexOfSecondBracket);
			return Integer.valueOf(indexText);			
			
		}
		
		// No square brackets, so just return 1 because this will be the first occurrence.
		return 1;
		
	}
	
	 /**
	 * Return xpath with modified parent index.
	 * 
	 * @param tag
	 *            the tag
	 * @param newIndex
	 *            the new index
	 * @return <code>String</code> xpath with altered index, for example:
	 *         /registryObjects/registryObject[1]/collection[1]/description[2]
	 * @throws Exception
	 *             the exception
	 * @since 1.0
	 */ 
//	 public static String setIndex(
//			 final String tag,
//			 final int newIndex) throws Exception{
//		 
//		 if((tag == null) || (tag.isEmpty())){
//			 throw new Exception("Requires valid tag");
//		 }
//		 
//		 return tag.replace(String.format("[%d]", getLastIndex(tag)), String.format("[%d]", newIndex));
//	 }
	
	/**
	 * xpath may be of form:
	 * /registryObjects/registryObject[1]/@group,"Atlas of Living Australia".
	 * 
	 * @param tag
	 *            the tag
	 * @return tag Tag without "[n]", for example: "registryObject" if tag is
	 *         "registryObject[1]"; or "registryObjects" if tag is
	 *         "registryObjects".
	 * @throws Exception
	 *             the exception
	 * @since 1.0
	 */
	public static String getTag(
			final String tag) 
					throws Exception{

		// Determine tag if there are square brackets
		if(tag.contains("[")){
			if(!tag.contains("]")){
				throw new Exception("tag "+tag+" contains \'[\' but not \']\'.");
			}
			
			int indexOfFirstBracket = tag.indexOf("[");
			String subTag = tag.substring(0, indexOfFirstBracket);
			validateTagName(subTag, "^[\\w@:]+$");
			return subTag;
		}
		
		// No square brackets, so just return tag name.
		validateTagName(tag, "^[\\w@:]+$");
		return tag;
		
	}
	
	
	/**
	 * Creates or returns a Node on Document according to xpath provided.
	 * 
	 * @param doc
	 *            the doc
	 * @param xpath
	 *            the xpath
	 * @param value
	 *            the value
	 * @throws Exception
	 *             the exception
	 * @since 1.0
	 */
	public void createXmlAsPerXpath(
			final String namespaceURI,
			final String xpath, 
			final String value) 
					throws Exception
	{
		if(value == null){ // Though it may be an empty string
			throw new Exception("Null value");
		}
		
		if(xpath == null){ 
			throw new Exception("Null xpath");
		}
		
		if(xpath.isEmpty()){ 
			throw new Exception("Requires valid xpath");
		}
		
		validateXpath(xpath); // Throws
		
		System.out.println("Processing xpath name: ["+xpath+"].");
		
		String xpathSub = xpath.substring(1);
		String[] tagList = xpathSub.split("/");
		if((tagList == null) || (tagList.length <= 0)){
			throw new Exception("Unable to obtain tag names from xpath ["+xpath+"].");
		}
			
		Node parentNode = _document;
		assert(parentNode.getNodeType() == Node.DOCUMENT_NODE);
		int tagsRemaining = tagList.length;
		for (String tag : tagList){
			// Move on down the xpath, creating and adding elements to the document until the last element or attribute.
			validateTagName(tag, "^[\\w@:\\]\\[]+$"); // Throws
			assert(parentNode != null);
			if(--tagsRemaining == 0){
				addXmlFinalNode(namespaceURI, _document, parentNode, tag, value);
			}
			else{
				parentNode = addXmlMiddleNode(namespaceURI, _document, parentNode, tag);
				assert(parentNode.getNodeType() == Node.ELEMENT_NODE); // Needs to be valid for next appending
			}
		}
	 }
	
	/**
	 * Creates a text <code>Node</code> of value provided and appends to
	 * <code>Node</code> provided.
	 * 
	 * @param doc
	 *            the doc
	 * @param node
	 *            the node
	 * @param value
	 *            the value
	 * @throws Exception
	 *             the exception
	 * @since 1.0
	 */
	private static void setElementText(
			Document doc, 
			Node node, 
			final String value) 
					throws Exception{
		
		if(node == null){
			assert(false);
			throw new Exception("Invalid node.");	
		}
		
		if((value == null) || (value.length() <= 0)){
			assert(false);
			throw new Exception("Invalid value.");	
		}
		
		System.out.println("Setting text content of value ["+value+"] on node ["+node.getNodeName()+"].");
		// setTextContent: On setting, any possible children this node may have are removed and, 
		// if it the new string is not empty or null, replaced by a single Text node containing 
		// the string this attribute is set to. 
		node.setTextContent(value);
	}
	
	
	/**
	 * Return youngest node name of xpath.
	 * 
	 * @param xpath
	 *            the xpath
	 * @return <String> youngest node name, for example: "description[1]"
	 * @throws Exception
	 *             the exception
	 * @see
	 * @since 1.0
	 */
	public static String getYoungestNodeName(
			final String xpath)
				throws Exception{
		
		
		String[] tagList = xpath.split("/");
		if((tagList == null) || (tagList.length <= 0)){
			throw new Exception("Unable to obtain tag names from xpath ["+xpath+"].");
		}
		
		String lastTag = tagList[tagList.length-1];
		if(lastTag.startsWith("@")){
			assert(tagList.length >= 2);
			String secondLastTag = tagList[tagList.length-2];
			assert(!secondLastTag.startsWith("@")); // Unexpected to have second last tag be an attribute :/
			return secondLastTag;
		}
		
		return lastTag;
		
	}
	
	/**
	 * Return youngest indexed node name of xpath.
	 * 
	 * @param xpath
	 *            the xpath
	 * @return <String> youngest node name, for example: "description[1]"
	 * @throws Exception
	 *             the exception
	 * @see
	 * @since 1.0
	 */
	public static String getYoungestIndexedNodeName(
			final String xpath)
				throws Exception{
		
		
		String[] tagArray = xpath.split("/");
		if((tagArray == null) || (tagArray.length <= 0)){
			throw new Exception("Unable to obtain tag names from xpath ["+xpath+"].");
		}
		
		List<String> tagList = Arrays.asList(tagArray);
		ListIterator<String> tagListIt = tagList.listIterator(tagList.size());
		
		// Iterate in reverse.
		while(tagListIt.hasPrevious()) {
			String tag = tagListIt.previous();
			if((tag.contains("[") && tag.contains("]"))){
				return tag;
			}
		}
		
		throw new Exception("Unable to determine youngest indexed node name from xpath ["+xpath+"]");
		
	}
	
	/**
	 * Return value of child specified by xpath.
	 * 
	 * @param parentNode
	 *            the parent node
	 * @param xpath
	 *            the xpath
	 * @return <code>String</code> value of element specified by xpath.
	 * @throws Exception
	 *             the exception
	 * @see
	 * @since 1.0
	 */
	public static List<String> getElementTextNS(
			final NamespaceContext nsContext,
			final Node parentNode,
			final String xpath)
					throws Exception{
		
		assert(nsContext != null);
		assert(parentNode != null);
		assert(xpath != null);
		
		NodeList nodeList = getElementListNS(nsContext, parentNode, xpath);
		List<String> nodeValueList = new ArrayList<String>();
		for(int i=0; i<nodeList.getLength(); i++){
			Node node = nodeList.item(i);
			String text = node.getTextContent();
			nodeValueList.add(text.trim());
		}
		
		return nodeValueList;
	}
	
	/**
	 * Gets the document element text ns.
	 * 
	 * @param xpath
	 *            the xpath
	 * @return the document element text ns
	 * @throws Exception
	 *             the exception
	 */
	public List<String> getDocumentElementTextNS(
			final String xpath)
					throws Exception{
		
		assert(_document != null); // provided during construction
		assert(_nsContext != null);
		return getElementTextNS(_nsContext, _document.getDocumentElement(), xpath);
	
	}
	
	/**
	 * Gets the document element list ns.
	 * 
	 * @param xpath
	 *            the xpath
	 * @return the document element list ns
	 * @throws Exception
	 *             the exception
	 */
	public NodeList getDocumentElementListNS(
			final String xpath)
					throws Exception{
		
		assert(_document != null); // provided during construction
		assert(_nsContext != null);
		return getElementListNS(_nsContext, _document.getDocumentElement(), xpath);
	
	}
	
	/**
	 * Gets the attribute node ns.
	 * 
	 * @param parentNode
	 *            the parent node
	 * @param xpath
	 *            the xpath
	 * @return the element list ns
	 * @throws Exception
	 *             the exception
	 */
	public static Node getAttributeNodeNS(
			final NamespaceContext nsContext,
			final Node parentNode,
			final String xpath)
					throws Exception{
		
		if(nsContext == null){
			throw new Exception("Requires valid Namespace Context");
		}
		
		if(parentNode == null){
			throw new Exception("Requires valid parentNode");
		}
		
		if((xpath == null) || (xpath.isEmpty())){
			throw new Exception("Requires xpath");
		}
		
		XPath objXPath = XPathFactory.newInstance().newXPath();
		assert(objXPath != null);
		
		objXPath.setNamespaceContext(nsContext);
		
		try
		{
			Node node = (Node) objXPath.evaluate(xpath, parentNode, XPathConstants.NODE);
		
			assert(node != null);
			
			if(node != null){
				assert(node.getNodeName() != null);
				System.out.println("Found attribute node "+node.getNodeName()+" for xpath "+xpath+" on parent node "+parentNode.getNodeName());
				return node;
			}
						
			return null;
			
		}
		catch(XPathExpressionException e){
			e.printStackTrace();
			throw new Exception("Unable to construct XPathExpression ["+xpath+"]- Exception: ["+e.getCause()+"].");
		}
	
	}
	
	public NodeList getElementListFromRootNS(
			final String xpath)
					throws Exception{
		
		
		assert(_nsContext != null);
		assert(_document != null);
		return getElementListNS(_nsContext, _document.getDocumentElement(), xpath);
		
	}
	
	
	/**
	 * Gets the element list ns.
	 * 
	 * @param parentNode
	 *            the parent node
	 * @param xpath
	 *            the xpath
	 * @return the element list ns
	 * @throws Exception
	 *             the exception
	 */
	public static NodeList getElementListNS(
			final NamespaceContext nsContext,
			final Node parentNode,
			final String xpath)
					throws Exception{
		
		if(nsContext == null){
			throw new Exception("Requires valid namespace context");
		}
		
		if(parentNode == null){
			throw new Exception("Requires valid parentNode");
		}
		
		if((xpath == null) || (xpath.isEmpty())){
			throw new Exception("Requires xpath");
		}
		
		XPath objXPath = XPathFactory.newInstance().newXPath();
		assert(objXPath != null);
		objXPath.setNamespaceContext(nsContext);
				
		try
		{
			NodeList nodeList = (NodeList) objXPath.evaluate(xpath, parentNode, XPathConstants.NODESET);
		
			assert(nodeList != null);
			
			System.out.println(nodeList.getLength()+" nodes found for xpath "+xpath+" on parent node "+parentNode.getNodeName());
//			if((nodeList == null) || (nodeList.getLength() <= 0)){ 
//				throw new Exception("Unable to obtain any node for xpath ["+xpath+"] from parentNode ["+parentNode.getNodeName()+"].");
//			}
			
			return nodeList;
			
		}
		catch(XPathExpressionException e){
			e.printStackTrace();
			throw new Exception("Unable to construct XPathExpression ["+xpath+"]- Exception: ["+e.getCause()+"].");
		}
		
		
	}
	
	/**
	 * Prints the child node info.
	 * 
	 * @param nodeList
	 *            the node list
	 */
	public static void printChildNodeInfo(final Node node){

		System.out.println("Node - name ["+node.getNodeName()+"], type: ["+node.getNodeType()+"], value: ["+node.getNodeValue()+"]\n");
      
		NodeList children = node.getChildNodes();
		for(int i = 0; i < children.getLength(); i++)
		{
    	  printChildNodeInfo(children.item(i));
    	}
	}
	
	/**
	 * Uri list from uri.
	 * 
	 * @param uri
	 *            the uri
	 * @return the list
	 * @throws Exception
	 *             the exception
	 */
	public static List<String> uriListFromUri(final String uri) throws Exception{
		
		List<String> uriList = new ArrayList<String>();
		org.jsoup.nodes.Document doc = Jsoup.connect(uri).get();
		assert(doc != null);
		
		Elements links = doc.select("a[href]");
				
		System.out.println(links.size());
        for (org.jsoup.nodes.Element link : links) {
        	String href = link.attr("abs:href");
        	if(!href.isEmpty()){
        		if(href.endsWith("xml")){ // skip link to parent directory 
        			System.out.println("Adding href" + href);;
        			uriList.add(href);
        		}
        	}
        }
        
        if(uriList.isEmpty()){
        	throw new Exception("No links at uri "+uri);
        }
        return uriList;
        
	}
	
	
	/**
	 * Return value of child specified by xpath.
	 * 
	 * @param path
	 *            the path
	 * @return value for this xpath
	 * @throws Exception
	 *             the exception
	 * @since 1.0
	 */
	@Override
	public List<String> contentListFromPath(String path) throws Exception {
		
		if(_document == null){
			assert(false);
			throw new Exception("Requires valid document object");
		}
		
		if(_nsContext == null){
			assert(false);
			throw new Exception("Requires valid NamespaceContext object");
		}
		
		 return getElementTextNS(_nsContext, _document.getDocumentElement(), path);
		
	}

	/* (non-Javadoc)
	 * @see org.ands.input.InputHelper#getLookUpNodeValue(java.lang.String, java.lang.String)
	 */
	@Override
	public List<String> getLookUpNodeValue(String lookUpUri, String treePath)
			throws Exception {
		throw new Exception("Not yet implemented!");
	}
	
	
	/**
	 * Removes the white space from node text.
	 * 
	 * @param xpath
	 *            the xpath
	 * @param node
	 *            the node
	 * @throws Exception
	 *             the exception
	 */
	public static void removeWhiteSpaceFromNodeText(final NamespaceContext nsContext, final String xpath, Node node) throws Exception{
		
		assert(node != null);
		assert(xpath != null);
		assert(!xpath.isEmpty());
		
		NodeList nodeList = getElementListNS(nsContext, node, xpath);
		assert(nodeList != null);
		for(int i = nodeList.getLength()-1; i >= 0; i--) {
			Node currentNode = nodeList.item(i);
			Node textNode = currentNode.getFirstChild();
			if(textNode != null){
				assert(textNode.getNodeType() == Node.TEXT_NODE);
				assert(textNode.getChildNodes().getLength() == 0);
				String text = textNode.getNodeValue();
				String textNoWhiteSpace = text.replaceAll("\\s", "");
				currentNode.setTextContent(textNoWhiteSpace);
				assert(currentNode.getTextContent().equals(textNoWhiteSpace));
			}
		}
		
	}

	/**
	 * Removes the node no children.
	 * 
	 * @param xpath
	 *            the xpath
	 * @param node
	 *            the node
	 * @throws Exception
	 *             the exception
	 */
	public static void removeNodeNoChildren(final NamespaceContext nsContext, final String xpath, Node node) throws Exception{
		
		assert(node != null);
		assert(xpath != null);
		assert(!xpath.isEmpty());
		
		NodeList nodeList = getElementListNS(nsContext, node, xpath);
		System.out.println("{Remove Node} seeking children on "+xpath);
		assert(nodeList != null);
		for(int i = nodeList.getLength()-1; i >= 0; i--) {
			Node currentNode = nodeList.item(i);
			assert(currentNode != null);
			
			// It may have a null text node, or empty text within a valid text node;
			// that's OK.. delete this node if it has no children of type Element
			
			if(!hasElementChild(currentNode)){
				
				// We have been requested to remove this node (of specific xpath) if it 
				// has no children.  Alert the caller if we have text content.  Unexpected :/.
				String textContent = currentNode.getTextContent();
				assert((textContent == null) || textContent.trim().isEmpty());
				
				System.out.println("{Remove Node} removing "+xpath+" because it has no children of type Element");
				currentNode.getParentNode().removeChild(currentNode);
				continue;
			}
			
			System.out.println("{Remove Node} node "+currentNode.getNodeName()+" has child of type Element");
			
		}
		
	}
	
	/**
	 * Checks for element child.
	 * 
	 * @param node
	 *            the node
	 * @return true, if successful
	 */
	private static boolean hasElementChild(final Node node){
		NodeList children = node.getChildNodes();
		for(int j = 0; j < children.getLength(); ++j){
			Node childNode = children.item(j);
			if(childNode.getNodeType() == Node.ELEMENT_NODE){
				return true;
			}
		}
		
		return false;
	}
	
	
	/**
	 * Removes the attribute no text.
	 * 
	 * @param xpath
	 *            the xpath
	 * @param node
	 *            the node
	 * @throws Exception
	 *             the exception
	 */
	public static void removeAttributeNoText(final NamespaceContext nsContext, String xpath, Node node) throws Exception {
		assert(node != null);
		assert(xpath != null);
		assert(!xpath.isEmpty());
		
		System.out.println("{Remove Node} seeking attribute "+xpath+" on node "+node.getNodeName());
		
		Node attributeNode = getAttributeNodeNS(nsContext, node, xpath);
		if(attributeNode != null){ // found
			assert(false);
			assert(attributeNode != null);
			assert((attributeNode.getNodeValue() == null) || (attributeNode.getNodeValue().isEmpty()));
			
			Element parentElement = (Element)attributeNode.getParentNode();
			parentElement.removeAttribute(attributeNode.getNodeName());
			System.out.println("{Remove Node} attribute "+attributeNode.getNodeName()+" from parent "+parentElement.getNodeName()+" because it has no value");
		}
	}
	
	
	/**
	 * Removes the node no text.
	 * 
	 * @param xpath
	 *            the xpath
	 * @param node
	 *            the node
	 * @throws Exception
	 *             the exception
	 */
	public static void removeNodeNoText(final NamespaceContext nsContext, final String xpath, Node node) throws Exception{
		assert(node != null);
		assert(xpath != null);
		assert(!xpath.isEmpty());
		
		NodeList nodeList = getElementListNS(nsContext, node, xpath);
		assert(nodeList != null);
		for(int i = nodeList.getLength()-1; i >= 0; i--) {
			System.out.println("{Remove Node} seeking text on occurence "+i+" of xpath "+xpath);
			Node currentNode = nodeList.item(i);
			assert(currentNode != null);
			
			String textContent = currentNode.getTextContent();
			if((textContent == null) || (textContent.isEmpty())){
				System.out.println("{Remove Node} "+xpath+" because it has no text");
				currentNode.getParentNode().removeChild(currentNode);
			}
		}
	}
	
	/**
	 * Merges any objects that are duplicated, determined by child element
	 * 'key'. For example, if you have many relatedObject elements that have the
	 * same value within their child element key, keep only one relatedObject
	 * element, and append all children from others relatedObject elements (so,
	 * take all child elements of name 'relation' and add them all to the one
	 * relatedObject element that we are maintaining. Remove remaining
	 * relatedObject elements.
	 * 
	 * @param xpath
	 *            the xpath
	 * @param topNode
	 *            the top node
	 * @throws Exception
	 *             the exception
	 */
	public static void mergeNodes(final NamespaceContext nsContext, final String namespaceUri, final String xpath, Node topNode) throws Exception{
		
		assert(nsContext != null);
		assert(topNode != null);
		assert(xpath != null);
		assert(!xpath.isEmpty());
		
		// First, find all relatedObject keys
		List<String> keyList = new ArrayList<String>();
		
		NodeList nodeList = getElementListNS(nsContext, topNode, xpath);
		for(int i=0; i < nodeList.getLength(); ++i){ // For each object with a key
			Node currentNode = nodeList.item(i);
			assert(currentNode != null);
			System.out.println("{Merge Nodes} Finding key on node "+xpath);
			NodeList keyNodeList = getElementListNS(nsContext, currentNode, String.format("./%s:key",nsContext.getPrefix(namespaceUri)));
			assert(keyNodeList.getLength() == 1);
			Node keyNode = keyNodeList.item(0);
			assert(keyNode != null);
			String key = keyNode.getTextContent();
			assert(key != null);
			assert(!key.isEmpty());
			if(!keyList.contains(key)){
				keyList.add(key);
			}
		}
		
		for(String k : keyList){
			
			List<Node> childFromRemovedList = new ArrayList<Node>();
			
			System.out.println("{Merge Nodes} Key " + k);
			
			// Now try to get a list of relatedObject elements all with the same key :/
			String xpathWithKey = ""+xpath+"["+nsContext.getPrefix(namespaceUri)+":key=\""+k+"\"]";
			System.out.println("{Merge Nodes} Finding object such that "+xpathWithKey);
				
			NodeList objsSameKey = getElementListNS(nsContext, topNode, xpathWithKey);
			assert(objsSameKey.getLength() > 0); 
			
			// Loop through all relatedObject objects of same key
			// We will be keeping the first, only, and appending all 
			// children elements from the others to it (except for the key element)
			for (int i = objsSameKey.getLength()-1; i >= 0; i--) {
				Node objNode = objsSameKey.item(i);
				assert(objNode != null);
				if(i > 0){
					// We will be removing this node, but first grab all children, apart
					// from key, because we will be appending them to the node which remains
					NodeList childNodeList = objNode.getChildNodes();
					for (int j = 0; j < childNodeList.getLength(); j++) {
						Node childNode = childNodeList.item(j);
						assert(childNode != null);
						if(childNode.getNodeName().equals("key")){
							System.out.println("{Merge Node} Skipping the ["+childNode.getNodeName()+"] node");
							continue;
						}
					
						System.out.println("{Merge Node} Saving child node ["+childNode.getNodeName()+"]");
						childFromRemovedList.add(childNode.cloneNode(true));
						
					}
					
					System.out.println("{Merge Node} Merging and deleting repeated occurence ["+objNode.getNodeName()+"]");
					objNode.getParentNode().removeChild(objNode);
					
				}
				else{
					System.out.println("{Merge Node} last one, so appending "+childFromRemovedList.size()+" child nodes from deleted nodes, and leaving this here ["+objNode.getNodeName()+"]");
					for(Node childFromDeleted: childFromRemovedList){
						System.out.println("{Merge Node} appending "+childFromDeleted.getNodeName()+" child node from deleted nodes");
						objNode.getOwnerDocument().adoptNode(childFromDeleted);
						objNode.appendChild(childFromDeleted);
					}
				}
			}
			
		}
	}

	/**
	 * Removes the node no specific child.
	 * 
	 * @param xpath
	 *            the xpath
	 * @param topNode
	 *            the top node
	 * @param xpathSpecificChild
	 *            the xpath specific child
	 * @throws Exception
	 *             the exception
	 */
	public static void removeNodeNoSpecificChild(final NamespaceContext nsContext, final String xpath, Node topNode, final List<String> xpathSpecificChildList) throws Exception{
		
		System.out.println("{Remove Node} specific child processing for "+xpath+" topNode "+topNode);
		NodeList nodeList = getElementListNS(nsContext, topNode, xpath);
		for (int i = nodeList.getLength()-1; i >= 0; i--) {
			Node currentNode = nodeList.item(i);
			assert(currentNode != null);
			if(requiresRemovalNoSpecificChild(nsContext, xpathSpecificChildList, currentNode)){
				currentNode.getParentNode().removeChild(currentNode);
			}
		}
		
	}
	
	private static boolean requiresRemovalNoSpecificChild(final NamespaceContext nsContext, final List<String> xpathSpecificChildList, Node currentNode) throws Exception{
		
		for(String xpathChild : xpathSpecificChildList){
			System.out.println("{Remove Node} seeking specific child "+xpathChild+" on xpath "+xpathChild);
			NodeList childNodeList = getElementListNS(nsContext, currentNode, xpathChild);
			if((childNodeList == null) || (childNodeList.getLength() <= 0)){
				System.out.println("{Remove Node} removing node "+currentNode.getNodeName()+" because it has no specific child "+xpathChild+" node");
				return true;
			}
		}
		
		return false;
	}
	
	public static void retainChildWithAttribute(final NamespaceContext nsContext, final String namespaceUri, final String xpath, final Map<String, String> attributeNameValuePair, Node node) throws Exception {
		
		
		// This child has children of the same name.  Retain the one that has the attribute expected if there is more than one.
		// If there are none, let it go.
		
		assert(node != null);
		NodeList nodeList = getElementListNS(nsContext, node, xpath);
		System.out.println("Found "+nodeList.getLength()+" nodes for xpath "+xpath);
		if(nodeList.getLength() <= 0){
			System.out.println("No children, so no need to prioritise");
		}
		
		List<Node> nodesWithAttributeNameValuePair = new ArrayList<Node>();
		List<Node> nodesOther = new ArrayList<Node>();
		
		for(int i = nodeList.getLength()-1; i >= 0; i--){
			Node child = nodeList.item(i);
			NamedNodeMap attributes  = child.getAttributes();
			assert(attributes != null);
			assert(attributes.getLength() > 0);
			for(int j = 0; j < attributes.getLength(); j++){
				Node attribute = attributes.item(j);
				assert(attribute.getNodeType() == Node.ATTRIBUTE_NODE);
				System.out.println(child.getNodeName()+" has attribute "+attribute.getNodeName()+" with value "+attribute.getNodeValue());
				for(Map.Entry<String, String> entry: attributeNameValuePair.entrySet()){
					System.out.println("Required attribute:  "+entry.getKey()+" with value "+entry.getValue());
					if((attribute.getNodeName().equals(entry.getKey()) && (attribute.getNodeValue().equals(entry.getValue())))){
						nodesWithAttributeNameValuePair.add(child);
					}
					else{
						nodesOther.add(child);
					}
				}
			}
		}
		
		assert(nodeList.getLength() == (nodesWithAttributeNameValuePair.size() + nodesOther.size()));
		
		// If we have a node with the attribute as required, remove the rest
		if(nodesWithAttributeNameValuePair.size() > 0){
			if(nodesOther.size() > 0){
				ListIterator<Node> iterator = nodesOther.listIterator(nodesOther.size());
				while(iterator.hasPrevious()){
					Node currentNode = iterator.previous();
					assert(currentNode != null);
					System.out.println("{Remove Node} removing node because other has priority due to attribute name and type "+ currentNode.toString());
					currentNode.getParentNode().removeChild(currentNode);
				}
			}
		
			NodeList nodeListAfterRemove = getElementListNS(nsContext, node, xpath);
			assert(nodeListAfterRemove.getLength() == 1);
		}
		
		String namespaceURI = node.getNamespaceURI();
		assert(namespaceURI != null);
		assert(!namespaceURI.isEmpty());
		
	}
	
	/**
	 * Split nodes always.
	 * 
	 * @param elementSplitRule
	 *            the element split rule
	 * @param node
	 *            the node
	 * @throws Exception
	 *             the exception
	 */
	public static void splitNodesAlways(final NamespaceContext nsContext, final String namespaceUri, ElementSplitRule elementSplitRule, Node node) throws Exception {
		
		assert(node != null);
		String xpath = elementSplitRule.getXpath();
		String nodeNameRepeatAt = elementSplitRule.getNodeNameRepeatAt();
		NodeList nodeList = getElementListNS(nsContext, node, xpath);
		Map<String, Boolean> splitRuleMap = elementSplitRule.getSplitRuleMap();
		assert(splitRuleMap != null);
		
		String namespaceURI = node.getNamespaceURI();
		assert(namespaceURI != null);
		assert(!namespaceURI.isEmpty());
		
		Utilities utilities = new Utilities();
		
		for (int i = nodeList.getLength()-1; i >= 0; i--) {
			Node childNode = nodeList.item(i);
			assert(childNode != null);
			String childName = childNode.getNodeName();
			System.out.println("{Adding Node} splitting at node "+childName);
			List<String> values = new ArrayList<String>();
			if(childNode.hasChildNodes()){
				Node textNode = childNode.getFirstChild();
				assert(textNode.getNodeType() == Node.TEXT_NODE); // The caller has requested a split, so this ought to be a text node here
				String value = textNode.getNodeValue();
				
				values = utilities.splitValueList(splitRuleMap, value);
				if(elementSplitRule.getIgnoreDuplicates()){
					values = utilities.deDuplicateList(values, true/*ignore case*/);
				}
				
				String firstValue = values.get(0);
				assert(firstValue != null);
				assert(firstValue.length() <= elementSplitRule.getMaxLen());
				childNode.setTextContent(firstValue);  
			}
			
			Node nodeToRepeat = nodeToRepeatAt(childNode, xpath, nodeNameRepeatAt);
			int requiredSiblingIndex = 1;
			// Now append some children for the remaining split values
			for(int k = values.size()-1 ; k >= 1; k--){
				String nextValue = values.get(k);
				assert(nextValue != null);
				Node topNode = getNextTopNode(nodeToRepeat, requiredSiblingIndex);
				Node nodeToAmend = nodeToAmend(nsContext, namespaceUri, nodeToRepeat, topNode, childName);
				assert(nodeToAmend != null);
				nodeToAmend.setTextContent(nextValue);
				assert(nextValue.length() <= elementSplitRule.getMaxLen());
				System.out.println("{Adding Node} adding node "+nodeNameRepeatAt+" because its value had to be split");
				requiredSiblingIndex++;
				
			}
		}
	}
	
	private static Node getNextTopNode(final Node nodeToRepeat, final int requiredSiblingIndex){
		// Look for next sibling of this name, and return that; otherwise, construct a next sibling via cloning, and return that
		int currentSiblingIndex = 1;
		Node lastSibling = nodeToRepeat.getNextSibling();
		while(lastSibling != null){
			
			if(lastSibling.getNodeName() == nodeToRepeat.getNodeName()){
				if(currentSiblingIndex == requiredSiblingIndex){
					assert(lastSibling != null);
					System.out.println("Found sibling of same name "+ nodeToRepeat.getNodeName()+" at index of "+requiredSiblingIndex);
					return lastSibling;
				}
				currentSiblingIndex++;
			}
			
			lastSibling = lastSibling.getNextSibling();

		}
		
		assert(lastSibling == null);
		
		Node clonedTopNode = nodeToRepeat.cloneNode(false);
		Node topNode = nodeToRepeat.getParentNode().insertBefore(clonedTopNode, nodeToRepeat.getNextSibling());
		assert(topNode.getNodeName() == nodeToRepeat.getNodeName());
		return topNode;
	}
	
	/**
	 * Split attribute always.
	 * 
	 * @param elementSplitRule
	 *            the element split rule
	 * @param node
	 *            the node
	 * @throws Exception
	 *             the exception
	 */
	public static void splitAttributeAlways(final NamespaceContext nsContext, final String namespaceUri, ElementSplitRule elementSplitRule, Node node) throws Exception {
		
		assert(node != null);
		String xpath = elementSplitRule.getXpath();
		String nodeNameRepeatAt = elementSplitRule.getNodeNameRepeatAt();
		NodeList nodeList = getElementListNS(nsContext, node, xpath);
		Map<String, Boolean> splitRuleMap = elementSplitRule.getSplitRuleMap();
		assert(splitRuleMap != null);
		
		Utilities utilities = new Utilities();
		
		for (int i = nodeList.getLength()-1; i >= 0; i--) {
			
			Node childNode = nodeList.item(i);
			assert(childNode != null);
			String childName = childNode.getNodeName();
			System.out.println("{Adding Node} splitting at attribute "+childName);
			
			NamedNodeMap namedNodeMap = childNode.getAttributes();
			if((namedNodeMap == null) || (namedNodeMap.getLength() <= 0)){
				continue;
			}
			
			String attributeName = "";
			String attributeValue = "";
			
			List<String> values = new ArrayList<String>(); 
			for(int j = namedNodeMap.getLength()-1; j >= 0; j--){
				Node namedNode = namedNodeMap.item(j);
				assert(namedNode != null);
				attributeName = namedNode.getNodeName();
				assert(attributeName != null);
				if(xpath.contains(attributeName)){
					attributeValue = namedNode.getNodeValue();
					if(attributeValue != null){
						values = utilities.splitValueList(splitRuleMap, attributeValue);
						if(elementSplitRule.getIgnoreDuplicates()){
							values = utilities.deDuplicateList(values, true/*ignore case*/);
						}
						
						if(values.size() > 1){ // Value required a split (if only one value, then this is the original value only, not split)
							String firstValue = values.get(0);
							assert(firstValue != null);
							assert(firstValue.length() <= elementSplitRule.getMaxLen());
							((Element)childNode).setAttribute(attributeName, firstValue);
						}
						
					}
					
				}
			}
			
			Node nodeToRepeat = nodeToRepeatAt(childNode, xpath, nodeNameRepeatAt);
			int requiredSiblingIndex = 1;
			// Now append some children for the remaining split values
			for(int k = 1 ; k < values.size(); k++){
				String nextValue = values.get(k);
				assert(nextValue != null);
				Node topNode = getNextTopNode(nodeToRepeat, requiredSiblingIndex);
				Node nodeToAmend = nodeToAmend(nsContext, namespaceUri, nodeToRepeat, topNode, childName);
				assert(nodeToAmend != null);
				((Element)nodeToAmend).setAttribute(attributeName, nextValue);
				assert(nextValue.length() <= elementSplitRule.getMaxLen());
				System.out.println("{Adding Node} adding node "+nodeNameRepeatAt+" with attribute "+attributeName+" and value "+nextValue+" because its value had to be split");
				nodeToRepeat.getParentNode().insertBefore(topNode, nodeToRepeat.getNextSibling());
				requiredSiblingIndex++;
			}
		}
	}
	
	/**
	 * Split nodes exceed max len.
	 * 
	 * @param elementSplitRule
	 *            the element split rule
	 * @param node
	 *            the node
	 * @throws Exception
	 *             the exception
	 */
	public static void splitNodesExceedMaxLen(final NamespaceContext nsContext, final String namespaceUri, ElementSplitRule elementSplitRule, Node node) throws Exception {
		
		assert(node != null);
		String xpath = elementSplitRule.getXpath();
		String nodeNameRepeatAt = elementSplitRule.getNodeNameRepeatAt();
		NodeList nodeList = getElementListNS(nsContext, node, xpath);
		Map<String, Boolean> splitRuleMap = elementSplitRule.getSplitRuleMap();
		assert(splitRuleMap != null);
		
		Utilities utilities = new Utilities();
		
		Map<String, List<String>> attributeMap = elementSplitRule.getAttributeMap();
		
		for (int i = nodeList.getLength()-1; i >= 0; i--) {
			int attributeIndex = 0;
			Node childNode = nodeList.item(i);
			assert(childNode != null);
			String childName = childNode.getNodeName();
			System.out.println("{Adding Node} splitting at node "+childName);
			List<String> values = new ArrayList<String>();
			if(childNode.hasChildNodes()){
				Node textNode = childNode.getFirstChild();
				assert(textNode.getNodeType() == Node.TEXT_NODE); // The caller has requested a split, so this ought to be a text node here
				String value = textNode.getNodeValue();
				if((value == null) || (value.isEmpty())){
					continue;
				}
				
				values = utilities.splitMaxLen(elementSplitRule, value);
				if(values.size() > 1){ // Value required a split (if only one value, then this is the original value only, not split)
					String firstValue = values.get(0);
					assert(firstValue != null);
					childNode.setTextContent(firstValue);  // Value required splitting, so set to first split value
					NamedNodeMap attributes = childNode.getAttributes();
					for(int l = 0; l < attributes.getLength(); l++){
						Node attribute = attributes.item(l);
						String newAttributeValue = getAttributeValueAtIndex(attributeMap, attribute.getNodeName(), attributeIndex++);
						((Element)childNode).setAttribute(attribute.getNodeName(), newAttributeValue);
					}
				}
			}
			
			// Now append some children for the remaining split values
			int requiredSiblingIndex = 1;
			Node nodeToRepeat = nodeToRepeatAt(childNode, xpath, nodeNameRepeatAt);
			for(int k = values.size()-1 ; k >= 1; k--){
				String nextValue = values.get(k);
				assert(nextValue != null);
				Node topNode = getNextTopNode(nodeToRepeat, requiredSiblingIndex);
				Node nodeToAmend = nodeToAmend(nsContext, namespaceUri, nodeToRepeat, topNode, childName);
				assert(nodeToAmend != null);
				nodeToAmend.setTextContent(nextValue);
				NamedNodeMap attributes = childNode.getAttributes();
				for(int l = 0; l < attributes.getLength(); l++){
					Node attribute = attributes.item(l);
					assert(attribute != null);
					String newAttributeValue = getAttributeValueAtIndex(attributeMap, attribute.getNodeName(), attributeIndex);
					((Element)nodeToAmend).setAttribute(attribute.getNodeName(), newAttributeValue);
				}
				System.out.println("{Adding Node} adding node "+childName+" because its value had to be split");
				attributeIndex++;
			}
		}
	
	}
	
	/**
	 * Node to repeat at.
	 * 
	 * @param childNode
	 *            the child node
	 * @param xpath
	 *            the xpath
	 * @param nodeNameRepeatAt
	 *            the node name repeat at
	 * @return the node
	 */
	private static Node nodeToRepeatAt(final Node childNode, String xpath, String nodeNameRepeatAt){
		
		if(childNode.getNodeName().equals(nodeNameRepeatAt)){
			System.out.println("Child node name: "+childNode.getNodeName()+", repeat at: "+nodeNameRepeatAt);
			return childNode;
		}
		
		Node nodeToRepeatAt = getAncestorWithName(childNode, nodeNameRepeatAt);
		assert(nodeToRepeatAt != null); // The child does not have an ancestor of this name
		return nodeToRepeatAt;
	}
	
	/**
	 * Gets the ancestor with name.
	 * 
	 * @param childNode
	 *            the child node
	 * @param ancestorNodeName
	 *            the ancestor node name
	 * @return the ancestor with name
	 */
	private static Node getAncestorWithName(final Node childNode, final String ancestorNodeName){
		if(childNode.getParentNode() == null){
			System.out.println(childNode.getNodeName()+" is orphaned");
			assert(false);
		}
		
		System.out.println("Seeking ancestor "+ancestorNodeName+" on node "+childNode.getNodeName());
		
		Node parentNode = childNode.getParentNode();
		if(parentNode == null){
			// We got to the top and haven't found the parent name that we are looking for
			System.out.println("Failed to find ancestor "+ancestorNodeName);
			assert(false);
		}
		
		if(parentNode.getNodeName().equals(ancestorNodeName)){
			return parentNode;
		}
		
		System.out.println("Found ancestor "+parentNode.getNodeName()+", continuing up...");
		return getAncestorWithName(parentNode, ancestorNodeName);
		
	}
	
	/**
	 * Elem to amend.
	 * 
	 * @param repeatedElement
	 *            the repeated element
	 * @param name
	 *            the name
	 * @return the element
	 * @throws Exception 
	 */
	private static Node nodeToAmend(final NamespaceContext nsContext, final String namespaceUri, final Node nodeToRepeat, final Node topNode, final String nodeName) throws Exception{
		
		
		assert(nsContext != null);
		assert(nodeToRepeat != null);
		assert(topNode != null);
		
		if(nodeName.equals(topNode.getNodeName())){
			return topNode;
		}
	
		String prefix = (nsContext.getPrefix(namespaceUri));
		assert(prefix != null);
		assert(!prefix.isEmpty());
		NodeList nodeList = getElementListNS(nsContext, topNode, String.format(".//%s:%s", prefix, nodeName));
		if(nodeList.getLength() <= 0){
			Node constructedElemToAmend = constructElemToAmend(nsContext, nodeToRepeat, topNode, nodeName, prefix);
			assert(constructedElemToAmend != null);
			return constructedElemToAmend;
		}
		
		assert(nodeList.getLength() <= 1); // Not handled yet - we've cloned the parent and we don't know how high the parent is above the child; we have cloned it deep and it may now have multiple of the children :/
		return nodeList.item(0);
	}
	
	/**
	 * When we are splitting values and spreading out over new nodes, we need to construct new nodes down the hierarchy because we do not clone
	 * in case we get more info than we want to repeat. For example, when splitting key and relation on a related object, you can't clone repeated
	 * nodes, otherwise if you split two contacts into two relatedObject when cloning you'll copy the relation info, then when this is split
	 * you will get both names having both relation types when there's only supposed to be one relation type per person
	 * 
	 * @param nodeToRepeat
	 *           
	 * @param repeatedTopElem
	 *           
	 * @param nodeName
	 *            
	 * @return newly constructed youngest node that follows the ancestry of the repeated element
	 * @throws Exception
	 *             the exception
	 */
	private static Node constructElemToAmend(final NamespaceContext nsContext, final Node nodeToRepeat, Node topNode, final String nodeName, final String prefix) throws Exception{
		
		assert(nodeToRepeat.getNodeName() == topNode.getNodeName());
		assert(topNode.getNodeName() != nodeName); // Ought to have been already found in this case
		
		// First make sure that this nodeToRepeat has the nodeName that we are looking for (unexpected not to)
		NodeList nodeList = getElementListNS(nsContext, nodeToRepeat, String.format(".//%s:%s", prefix, nodeName));
		assert(nodeList.getLength() > 0);
		
		// Take one node and examine the hierarchy
		Node node = nodeList.item(0);
		assert(node != null);
		
		List<String> tagHierarchyList = new ArrayList<String>();
		Node currentTop = node;
		while(currentTop.getNodeName() != nodeToRepeat.getNodeName()){
			System.out.println("Adding hierarchy tag "+ currentTop.getNodeName());
			tagHierarchyList.add(currentTop.getNodeName());
			currentTop = currentTop.getParentNode();
		}
		
		int index = 1; // todo - ok for all conditions?
		// Reverse through list adding children to our repeated node to replicate hierarchy
		Node youngestParent = topNode; // youngestParent must be pointer to repeatedTopElem - not a copy
		assert(youngestParent.equals((Node)topNode));
		assert(youngestParent.hashCode() == ((Node)topNode).hashCode());
		for(int i = tagHierarchyList.size() - 1; i >= 0; i--){
			String currentNodeNameToAdd = tagHierarchyList.get(i);
			youngestParent = createElementAtIndex(nsContext.getNamespaceURI(prefix), nodeToRepeat.getOwnerDocument(), youngestParent, currentNodeNameToAdd, index); 
		}
		
		
		return youngestParent;
		
	}
	/**
	 * Gets the attribute value at index.
	 * 
	 * @param attributeMap
	 *            the attribute map
	 * @param name
	 *            the name
	 * @param index
	 *            the index
	 * @return the attribute value at index
	 * @throws Exception
	 *             the exception
	 */
	private static String getAttributeValueAtIndex(Map<String, List<String>> attributeMap, final String name, final int index) throws Exception{
		
		for (Map.Entry<String, List<String>> entry : attributeMap.entrySet()){
			System.out.println("key "+entry.getKey()+" at index "+index+" in attribute Map");
			if(entry.getKey().equals(name)){
				List<String> values = entry.getValue();
				assert(values != null);
				assert(values.size() > index);
				String value = values.get(index);
				System.out.println("returning value"+value+" at index "+index+" for attribute of name "+name+" in attribute Map");
				return value;
			}
		}
		
		System.out.println("No value found at index "+index+" for "+name+" in attribute Map");
		assert(false);
		throw new Exception();
		
	}
	
	public void adoptNode(Node orphanNode) {
		// TODO Auto-generated method stub
		assert(_document != null);
		_document.adoptNode(orphanNode);
		
	}
		
}
